package DyingBaby.backend.dm;

import DyingBaby.backend.dm.page.Page;
import DyingBaby.backend.dm.page.PageOne;
import DyingBaby.backend.dm.page.PageX;
import DyingBaby.backend.common.AbstractCache;
import DyingBaby.backend.dm.dataItem.DataItem;
import DyingBaby.backend.dm.dataItem.DataItemImpl;
import DyingBaby.backend.dm.logger.Logger;
import DyingBaby.backend.dm.pageCache.PageCache;
import DyingBaby.backend.dm.pageIndex.PageIndex;
import DyingBaby.backend.dm.pageIndex.PageInfo;
import DyingBaby.backend.tm.TransactionManager;
import DyingBaby.common.Error;
import DyingBaby.utils.Panic;
import DyingBaby.utils.Types;

/**
 * DataManagerImpl 是 DataManager 接口的具体实现，负责管理数据项(DataItem)的存储和检索。
 * 它继承自 AbstractCache<DataItem>，提供了数据项缓存的具体实现。
 * 这个类是数据库存储引擎的核心，协调页面缓存、日志系统、事务管理器和页面索引的工作。
 */
public class DataManagerImpl extends AbstractCache<DataItem> implements DataManager {

    TransactionManager tm; // 事务管理器，用于管理事务的生命周期
    PageCache pc; // 页面缓存，用于管理数据库页面的内存缓存和磁盘I/O
    Logger logger; // 日志记录器，用于记录数据修改操作，支持故障恢复
    PageIndex pIndex; // 页面索引，用于管理空闲页面空间信息
    Page pageOne; // 第一页，通常包含数据库元数据和状态信息

    /**
     * 构造函数
     * @param pc 页面缓存实例
     * @param logger 日志记录器实例
     * @param tm 事务管理器实例
     */
    public DataManagerImpl(PageCache pc, Logger logger, TransactionManager tm) {
        super(0); // 调用父类构造函数，初始缓存大小为0（可能表示无限制或后续调整）
        this.pc = pc;
        this.logger = logger;
        this.tm = tm;
        this.pIndex = new PageIndex(); // 初始化页面索引
    }

    /**
     * 根据UID读取数据项，并检查其有效性
     * @param uid 数据项的唯一标识符
     * @return 读取到的数据项，如果数据项无效则返回null
     * @throws Exception 读取过程中可能发生的异常
     */
    @Override
    public DataItem read(long uid) throws Exception {
        // 从缓存中获取数据项
        DataItemImpl di = (DataItemImpl)super.get(uid);
        // 检查数据项是否有效（未被删除）
        if(!di.isValid()) {
            di.release(); // 释放无效数据项
            return null;
        }
        return di;
    }

    /**
     * 插入新数据
     * 1. 在pageIndex中获取一个足以存储插入内容的页面的页号
     * 2. 获取页面后，首先需要写入插入日志
     * 3. 通过pageX插入数据，并返回插入位置的偏移
     * 4. 将页面信息重新插入pageIndex
     * @param xid 事务ID
     * @param data 要插入的数据字节数组
     * @return 新插入数据项的唯一标识符(UID)
     * @throws Exception 插入过程中可能发生的异常
     */
    @Override
    public long insert(long xid, byte[] data) throws Exception {
        // 包装数据项，添加头部信息（有效位、数据长度等）
        byte[] raw = DataItem.wrapDataItemRaw(data);
        // 检查数据是否太大，无法存储在单个页面中
        if(raw.length > PageX.MAX_FREE_SPACE) {
            throw Error.DataTooLargeException;
        }

        PageInfo pi = null;
        // 循环尝试获取足够空间的页面（最多尝试5次）
        for(int i = 0; i < 5; i ++) {
            pi = pIndex.select(raw.length); // 从页面索引中选择有足够空间的页面
            if (pi != null) {
                break;
            } else {
                // 如果没有找到合适页面，创建新页面并添加到页面索引
                int newPgno = pc.newPage(PageX.initRaw());
                pIndex.add(newPgno, PageX.MAX_FREE_SPACE);
            }
        }
        if(pi == null) {
            throw Error.DatabaseBusyException; // 数据库繁忙，无法插入数据
        }

        Page pg = null;
        int freeSpace = 0;
        try {
            // 获取选中的页面
            pg = pc.getPage(pi.pgno);
            // 生成插入日志
            byte[] log = Recover.insertLog(xid, pg, raw);
            // 记录日志（先写日志，后写数据）
            logger.log(log);

            // 在页面中插入数据
            short offset = PageX.insert(pg, raw);

            // 释放页面
            pg.release();
            // 将页面号和偏移量组合成UID返回
            return Types.addressToUid(pi.pgno, offset);

        } finally {
            // 无论成功与否，都将页面信息重新插入页面索引
            if(pg != null) {
                pIndex.add(pi.pgno, PageX.getFreeSpace(pg));
            } else {
                pIndex.add(pi.pgno, freeSpace);
            }
        }
    }

    /**
     * 关闭数据管理器，释放所有资源
     */
    @Override
    public void close() {
        super.close(); // 关闭缓存，释放所有数据项
        logger.close(); // 关闭日志记录器

        // 设置第一页的版本控制信息为"已关闭"
        PageOne.setVcClose(pageOne);
        pageOne.release(); // 释放第一页
        pc.close(); // 关闭页面缓存
    }

    /**
     * 为数据项的更新操作生成日志
     * @param xid 事务ID
     * @param di 要记录日志的数据项
     */
    public void logDataItem(long xid, DataItem di) {
        // 生成更新日志
        byte[] log = Recover.updateLog(xid, di);
        // 记录日志
        logger.log(log);
    }

    /**
     * 释放数据项
     * @param di 要释放的数据项
     */
    public void releaseDataItem(DataItem di) {
        super.release(di.getUid());
    }

    /**
     * 当缓存未命中时，从数据库文件中读取数据项
     * @param uid 数据项的唯一标识符
     * @return 从文件读取的数据项对象
     * @throws Exception 读取过程中可能发生的异常
     */
    @Override
    protected DataItem getForCache(long uid) throws Exception {
        // 从UID中提取偏移量（低16位）
        short offset = (short)(uid & ((1L << 16) - 1));
        // 从UID中提取页面号（高32位）
        uid >>>= 32;
        int pgno = (int)(uid & ((1L << 32) - 1));
        // 获取页面
        Page pg = pc.getPage(pgno);
        // 从页面中解析数据项
        return DataItem.parseDataItem(pg, offset, this);
    }

    /**
     * 当数据项从缓存中释放时调用
     * @param di 要释放的数据项
     */
    @Override
    protected void releaseForCache(DataItem di) {
        di.page().release();
    }


    // 在创建文件时初始化第一页(PageOne)
    void initPageOne() {
        // 创建新页面，使用PageOne的初始化数据
        int pgno = pc.newPage(PageOne.InitRaw());
        assert pgno == 1; // 确保第一页的页号为1
        try {
            // 获取第一页
            pageOne = pc.getPage(pgno);
        } catch (Exception e) {
            Panic.panic(e);
        }
        // 将第一页刷写到磁盘
        pc.flushPage(pageOne);
    }

    /**
     * 在打开已有文件时读入第一页，并验证其正确性
     * @return 第一页验证是否通过
     */
    boolean loadCheckPageOne() {
        try {
            // 获取第一页（页号为1）
            pageOne = pc.getPage(1);
        } catch (Exception e) {
            Panic.panic(e);
        }
        // 检查第一页的版本控制信息
        return PageOne.checkVc(pageOne);
    }

    /**
     * 初始化页面索引，扫描所有页面并记录其空闲空间信息
     */
    void fillPageIndex() {
        // 获取数据库文件中的页面总数
        int pageNumber = pc.getPageNumber();
        // 从第2页开始扫描（第1页是特殊页面）
        for(int i = 2; i <= pageNumber; i ++) {
            Page pg = null;
            try {
                // 获取页面
                pg = pc.getPage(i);
            } catch (Exception e) {
                Panic.panic(e);
            }
            // 将页面信息和其空闲空间添加到页面索引
            pIndex.add(pg.getPageNumber(), PageX.getFreeSpace(pg));
            // 释放页面
            pg.release();
        }
    }
    
}
